<?php
/*------------------------------------------------------------------------------
 ActionResourceProxy.php 2020-10-19
 Gambio GmbH
 http://www.gambio.de
 Copyright (c) 2020 Gambio GmbH
 Released under the GNU General Public License (Version 2)
 [http://www.gnu.org/licenses/gpl-2.0.html]
 -----------------------------------------------------------------------------*/

declare(strict_types=1);

namespace Gambio\Admin\Modules\Dashboard\App\Actions;

use Curl\Curl;
use Gambio\Admin\Modules\Dashboard\Html\GambioAssetUrlReplacer;
use Gambio\Core\Application\Http\AbstractAction;
use Gambio\Core\Application\Http\Request;
use Gambio\Core\Application\Http\Response as HttpResponse;
use Throwable;

/**
 * Class ActionResourceProxy
 *
 * Proxies JS and CSS resources from Gambio servers with local caching.
 * Only allows requests to gambio.de or gambio.com domains for security.
 *
 * @package Gambio\Admin\Modules\Dashboard\App\Actions
 */
class ActionResourceProxy extends AbstractAction
{
    protected const CACHE_TTL_IN_MINUTES = 15;

    private const ALLOWED_CONTENT_TYPES = [
        'js'    => 'text/javascript',
        'css'   => 'text/css',
        'woff'  => 'font/woff',
        'woff2' => 'font/woff2',
        'ttf'   => 'font/ttf',
        'eot'   => 'application/vnd.ms-fontobject',
        'otf'   => 'font/otf',
    ];

    private Curl $curl;

    private string $cacheDirectory;


    /**
     * ActionResourceProxy constructor.
     *
     * @param Curl   $curl
     * @param string $cacheDirectory
     */
    public function __construct(Curl $curl, string $cacheDirectory)
    {
        $this->curl           = $curl;
        $this->cacheDirectory = rtrim($cacheDirectory, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
    }


    /**
     * @param Request      $request
     * @param HttpResponse $response
     *
     * @return HttpResponse
     */
    public function handle(Request $request, HttpResponse $response): HttpResponse
    {
        $url = $this->extractUrl($request);

        if ($url === null) {
            return $response->withStatus(400)->write('Missing path parameter');
        }

        if (!GambioAssetUrlReplacer::isAllowedGambioDomain($url)) {
            return $response->withStatus(403)->write('Forbidden: Only gambio.de or gambio.com domains are allowed');
        }

        if (!GambioAssetUrlReplacer::isAllowedFileType($url)) {
            return $response->withStatus(403)->write('Forbidden: File type not allowed');
        }

        $content = $this->getResourceWithCache($url);

        if ($content === null) {
            return $response->withStatus(502)->write('Failed to fetch resource');
        }

        return $response
            ->withHeader('Content-Type', $this->getContentType($url))
            ->withHeader('Cache-Control', 'public, max-age=' . (self::CACHE_TTL_IN_MINUTES * 60))
            ->write($content);
    }


    /**
     * Extracts the remote URL from the request path.
     * Path format: /admin/dashboard/resource/{host}/{path}
     * Example: /admin/dashboard/resource/dashboard.gambio.de/files/admin-news/js/file.js
     *
     * @param Request $request
     *
     * @return string|null
     */
    private function extractUrl(Request $request): ?string
    {
        $path = $request->getAttribute('path');

        if ($path === null || $path === '') {
            return null;
        }

        //eg: https://dashboard.gambio.de/files/admin-news/js/file.js
        return 'https://' . $path;
    }


    /**
     * Determines content type based on file extension.
     *
     * @param string $url
     *
     * @return string
     */
    private function getContentType(string $url): string
    {
        return self::ALLOWED_CONTENT_TYPES[$this->getFileExtension($url)] ?? 'text/javascript';
    }


    /**
     * Extracts the file extension from a URL.
     *
     * @param string $url
     *
     * @return string
     */
    private function getFileExtension(string $url): string
    {
        $path = parse_url($url, PHP_URL_PATH) ?? '';

        return strtolower(pathinfo($path, PATHINFO_EXTENSION));
    }


    /**
     * Gets resource content with caching support.
     *
     * @param string $url
     *
     * @return string|null
     */
    private function getResourceWithCache(string $url): ?string
    {
        $cacheFile = $this->getCacheFilePath($url);

        if ($this->isCacheValid($cacheFile)) {
            return file_get_contents($cacheFile);
        }

        $content = $this->fetchFromRemote($url);

        if ($content !== null) {
            file_put_contents($cacheFile, $content);
            return $content;
        }

        return $this->getStaleCache($cacheFile);
    }


    /**
     * Checks if the cache file is still valid.
     *
     * @param string $cacheFile
     *
     * @return bool
     */
    private function isCacheValid(string $cacheFile): bool
    {
        return file_exists($cacheFile)
            && time() - filemtime($cacheFile) < self::CACHE_TTL_IN_MINUTES * 60;
    }


    /**
     * Fetches content from remote URL.
     *
     * @param string $url
     *
     * @return string|null
     */
    private function fetchFromRemote(string $url): ?string
    {
        try {
            $content = $this->curl->get($url);

            if ($content !== null && $content !== '' && $content !== false) {
                return $content;
            }
        } catch (Throwable $exception) {
            // Fetch failed, will try stale cache
        }

        return null;
    }


    /**
     * Returns stale cache content if available.
     *
     * @param string $cacheFile
     *
     * @return string|null
     */
    private function getStaleCache(string $cacheFile): ?string
    {
        if (file_exists($cacheFile)) {
            touch($cacheFile);
            return file_get_contents($cacheFile);
        }

        return null;
    }


    /**
     * Generates a cache file path for the given URL.
     *
     * @param string $url
     *
     * @return string
     */
    private function getCacheFilePath(string $url): string
    {
        $hash      = md5($url);
        $extension = pathinfo(parse_url($url, PHP_URL_PATH) ?? '', PATHINFO_EXTENSION) ?: 'js';

        return $this->cacheDirectory . $hash . '.' . $extension;
    }
}
